<!--
 * @Description: ------------ fileDescription -----------
 * @Author: snows_l snows_l@163.com
 * @Date: 2024-08-08 10:56:18
 * @LastEditors: snows_l snows_l@163.com
 * @LastEditTime: 2025-01-17 19:53:09
 * @FilePath: /BLOG/src/views/article/detail.vue
-->
<template>
  <div class="article-detail-warp" id="layout-content">
    <PageTopCover
      :moduleTitle="'文章详情'"
      :textIcon="'📑'"
      :coverImg="getOnlineImgUrl('/imgs/bg/127.png')"
      :isMobile="isMobi"
      :mudulDesc="state.arcticleDetail.title"
      :isArticle="false"></PageTopCover>

    <div class="article-content-warp-out" v-if="valueHtml" :class="{ 'm-article-content-warp-out': isMobi }">
      <div class="article-content-warp" v-loading="state.loading">
        <div class="article-content">
          <div class="article-cover-warp" :class="{ 'm-article-cover-warp': isMobi }">
            <div class="cover-warp pointer">
              <LImg class="cover-img" :src="state.arcticleDetail.cover" />
            </div>
            <!-- <div class="cover-title">COVER</div> -->
          </div>
          <Editor id="editor" class="editor-content-warp" v-model="valueHtml" :defaultConfig="editorConfig" :mode="mode" @onCreated="handleCreated" />
          <div class="article-intfo">
            <div class="pointer">
              <IconHot class="iconfont"></IconHot>
              <span>{{ state.arcticleDetail.readCount || 0 }}</span>
            </div>
            <div class="pointer" @click="handleAdd('comment')">
              <IconComment :size="18" class="iconfont"></IconComment>
              <span>{{ state.arcticleDetail.commentCount || 0 }}</span>
            </div>
            <div class="pointer" @click="handleAdd('share')">
              <IconLike :animation="state.isGood" :size="18" class="iconfont" />
              <span>{{ state.arcticleDetail.shareCount || 0 }}</span>
            </div>
            <div class="pointer kbn-custom" data-tip="点击 下载源代码" v-if="state.arcticleDetail.isPreview === 1" @click="downloadCode">
              <img class="down-icon" :class="{ disabled: state.loading }" style="width: 20px; height: 20px; margin-right: 10px" src="@/assets/images/icon/icon-down.svg" />
              <span class="" style="font-size: 12px">源码下载</span>
            </div>
            <div class="pointer kanban-custom" data-tip="点击 预览" v-if="state.arcticleDetail.isPreview === 1" @click="handleView">
              <IconPreview class="iconfont" :size="18"></IconPreview>
              <span class="" style="font-size: 12px">预览</span>
            </div>
          </div>
          <div v-if="!state.isMobile" class="flex-container">
            <div class="table-of-contents-warp">
              <div v-if="tableOfContents.length > 0" class="table-of-contents">
                <!-- 目录内容 -->
                <ul>
                  <li v-for="(item, index) in tableOfContents" :key="item.id" :style="{ paddingLeft: item.level * (state.isMobile ? 25 : 16) + 'px' }">
                    <!-- <a :class="{ active: activeIndex === index }" :href="`#${item.id}`" @click="handleItemClick(index)">{{ item.text }}</a> -->
                    <a
                      class="pointer"
                      :class="{ active: activeIndex === index }"
                      @click="
                        e => {
                          e.preventDefault();
                          handleItemClick(item, index);
                        }
                      ">
                      {{ item.text }}
                    </a>
                  </li>
                </ul>
              </div>
              <div v-else class="no-toc">
                <Empty :text="'暂无目录内容，请在文章中添加标题'" :loadingText="'目录内容正在生成中...'" :loading="state.loading" />
              </div>
            </div>
          </div>
        </div>
      </div>
      <CommentView style="margin-top: 15px" ref="commentViewRef" :articleId="state.arcticleDetail.id"></CommentView>
    </div>
    <div class="no-article" :class="{ 'm-no-article': isMobi }" v-else>
      <Empty :text="'暂无文章内容，请等待作者更新'" :loadingText="'文章内容正在拼命加载中...'" :loading="state.loading" />
    </div>
  </div>
</template>

<script lang="ts" setup>
import { addShareCount, getArticleDetail } from '@/api/article';
import { useAppStore } from '@/store/app';
import { copyToClipboard, downloadFileOfPreviewUrl, getImgIcon, getOnlineImgUrl, isMobile } from '@/utils/common';
import { Editor } from '@wangeditor/editor-for-vue';
import '@wangeditor/editor/dist/css/style.css'; // 引入 css
import { onBeforeUnmount, onMounted, onUpdated, reactive, ref, shallowRef, watch } from 'vue';
import { useRoute } from 'vue-router';

import useResize from '@/hooks/useResize';
const { isMobi } = useResize();
const appStore = useAppStore();

const commentViewRef = ref(null);
const route = useRoute();
let articleId = route.query.id;
const tableOfContents = ref([]);
const updateCount = ref(0);
const tocRihgt = ref('20px');

let scorllContainer: Element | null = null;

// 编辑器实例，必须用 shallowRef
const editorRef = shallowRef();
// 编辑模式
const mode = ref('default'); // simple | default
const valueHtml = ref('');

const state = reactive({
  loading: false,
  arcticleDetail: {},
  isMobile: isMobile(1580),
  commentList: [],
  isGood: false
});

// 目录等级映射
const levelMap = {
  H1: 1,
  H2: 1.5,
  H3: 2,
  H4: 3,
  H5: 4
};
// 目录等级反向映射
const levelReverseMap = {
  1: 'H1',
  1.5: 'H2',
  2: 'H3',
  3: 'H4',
  4: 'H5'
};

const toolbarConfig = {};
const editorConfig = {
  placeholder: '内容加载中...',
  readOnly: true,
  MENU_CONF: {}
};

// 获取文章详情
const getArticleDetailFn = () => {
  let serverUrl = appStore.projectConfig.bsServerUrl;
  state.loading = true;
  getArticleDetail(articleId)
    .then(res => {
      res.data.subTitle = res.data.subTitle.replace(/&#39;/g, "'");
      res.data.content = res.data.content.replace(/&#39;/g, "'");
      res.data.content = decodeURIComponent(res.data.content);
      state.arcticleDetail = res.data;
      valueHtml.value = state.arcticleDetail.content;

      state.arcticleDetail.cover =
        import.meta.env.VITE_CURRENT_ENV == 'dev' ? import.meta.env.VITE_DEV_BASE_SERVER + state.arcticleDetail.cover : serverUrl + state.arcticleDetail.cover;

      state.loading = false;
    })
    .finally(() => {
      state.loading = false;
    });
};

if (articleId) {
  getArticleDetailFn();
}

const handleAdd = (type: string) => {
  if (type === 'comment') {
    commentViewRef.value.clearTO();
  } else if (type === 'share') {
    addShareCount(articleId).then(res => {
      if (res.code == 200) {
        state.arcticleDetail.shareCount = res.data;
        state.isGood = true;
        setTimeout(() => {
          state.isGood = false;
        }, 1000);
      }
    });
  }
};

// 当前高亮的目录项索引
const activeIndex = ref(0);

// 添加复制按钮的逻辑 以及 mac os 系统的关闭按钮样式
const addCopyCodeAndMacosStyle = () => {
  const codeBlocks = document.querySelectorAll('#editor pre > code');
  codeBlocks.forEach(codeBlock => {
    codeBlock.style.borderTopLeftRadius = '0px';
    codeBlock.style.borderTopRightRadius = '0px';
    codeBlock.style.marginTop = '30px';
    // 创建复制按钮
    const copyButton = document.createElement('button');
    copyButton.innerText = '复制';
    copyButton.classList.add('copy-button');
    copyButton.classList.add('pointer');

    // 为复制按钮添加点击事件处理程序
    copyButton.addEventListener('click', () => {
      const codeText = codeBlock.querySelector('span').textContent;
      copyToClipboard({
        content: codeText,
        success: () => {
          // 修改按钮文本为 "已复制"
          copyButton.innerText = '已复制';
        },
        error: () => {}
      });

      // 延迟一段时间后恢复按钮文本为 "复制"
      setTimeout(() => {
        copyButton.innerText = '复制';
      }, 3000); // 毫秒为单位，您可以调整时间长度
    });

    // 为代码块添加macos系统的关闭按钮样式
    const codeTitle = document.createElement('div');
    codeTitle.style.position = 'absolute';
    codeTitle.classList.add('code-title');
    codeTitle.style.top = '-20px';
    codeTitle.style.left = '0px';
    codeTitle.style.width = '100%';
    codeTitle.style.height = '22px';
    codeTitle.style.lineHeight = '22px';
    codeTitle.style.backgroundColor = '#181818';
    codeTitle.style.borderTopLeftRadius = '4px';
    codeTitle.style.borderTopRightRadius = '4px';
    codeTitle.style.borderBottom = '0';
    codeTitle.style.overflow = 'hidden';
    const img = document.createElement('img');
    img.style.height = '100%';
    img.style.marginLeft = '5px';
    img.src = getImgIcon('macos-close.png');
    codeTitle.appendChild(img);

    // 将复制按钮添加到代码块的父级元素中
    codeBlock.appendChild(copyButton);
    codeBlock.appendChild(codeTitle);
  });
};

// 添加代码超过指定行数时自动折叠，点击展开按钮展开的逻辑
const addCodeFold = () => {
  const lineNum = 20; // 代码超过指定行数时自动折叠
  const codeBlocks = document.querySelectorAll('#editor pre > code');

  codeBlocks.forEach(codeBlock => {
    const lines = codeBlock.textContent.split('\n').length;
    if (lines > lineNum) {
      codeBlock.style.height = lineNum * 22 + 'px';
      codeBlock.style.marginBottom = '30px';
      codeBlock.style.overflowY = 'hidden';
      codeBlock.style.borderBottomLeftRadius = '0px';
      codeBlock.style.borderBottomRightRadius = '0px';
      codeBlock.style.transition = 'height 0.3s ease';
      const foldButton = document.createElement('div');
      foldButton.innerText = '一键展开';
      foldButton.classList.add('fold');
      foldButton.classList.add('pointer');
      foldButton.classList.add('code-expand');

      foldButton.style.backgroundColor = '#ccc';
      foldButton.style.color = '#333';
      foldButton.style.fontSize = '12px';
      foldButton.style.position = 'absolute';
      foldButton.style.width = '100%';
      foldButton.style.height = '22px';
      foldButton.style.lineHeight = '22px';
      foldButton.style.bottom = '-22px';
      foldButton.style.textAlign = 'center';
      foldButton.style.left = '0';
      foldButton.style.borderBottomLeftRadius = '4px';
      foldButton.style.borderBottomRightRadius = '4px';
      foldButton.style.borderTop = '0';
      // 为展开按钮添加点击事件处理程序
      foldButton.addEventListener('click', () => {
        foldButton.classList.toggle('fold');
        if (foldButton.classList.contains('fold')) {
          foldButton.innerText = '一键展开';
          codeBlock.style.height = lineNum * 22 + 'px';
          codeBlock.style.overflowY = 'hidden';
        } else {
          foldButton.innerText = '一键收起';
          codeBlock.style.height = 'auto';
          codeBlock.style.overflowY = 'visible';
        }
      });

      // 将展开按钮添加到代码块的父级元素中
      codeBlock.appendChild(foldButton);
    }
  });
};

// 添加锚点
const addAnchorLinks = () => {
  const headings = document.querySelectorAll('#editor h2, #editor h3, #editor h4, #editor h5');
  headings.forEach((heading, index) => {
    const anchorLink = document.createElement('a');
    anchorLink.setAttribute('href', `#section-${index + 1}`);
    // anchorLink.textContent = heading.textContent; // 设置锚点文本为标题文本
    anchorLink.style.pointerEvents = 'none'; // 设置 pointer-events 为 none，使链接不可点击

    // 设置标题的id属性
    heading.setAttribute('id', `section-${index + 1}`);

    // 将锚点链接插入到标题内
    heading.innerHTML = anchorLink.outerHTML + heading.innerHTML;
  });
};

// 更新目录项点击事件处理函数
const handleItemClick = (item, index: number) => {
  const container = document.querySelector('.table-of-contents-warp') as HTMLElement;
  const containerRect = container.getBoundingClientRect();
  activeIndex.value = index;
  const element = document.querySelector(`#section-${index + 1}`);
  const offsetTop = element.offsetTop + 360 + 420; //  360 是顶部导航栏高度 + 页面封面高度，420 是封面高度
  scorllContainer.scrollTop = offsetTop;
};

// 生成目录
const generateTableOfContents = () => {
  updateCount.value++; // 触发更新
  const headings = document.querySelectorAll('#editor h1, #editor h2, #editor h3, #editor h4, #editor h5');
  const toc = [];
  headings.forEach((heading, index) => {
    const id = `section-${index + 1}`;
    const level = levelMap[heading.tagName]; // 根据标题等级设置目录项的缩进
    heading.setAttribute('id', id); // 设置标题的id属性
    toc.push({ id: id, text: heading.textContent, level: level, index: index }); // 将标题文本、id和等级添加到目录项中
  });
  return toc;
};

// 下载源代码
const downloadCode = () => {
  let url = 'http://124.223.41.220:3333/html/preview.html';
  downloadFileOfPreviewUrl(url, 'preview.html');
};

// 监听滚动事件，更新目录项高亮状态
const handleScroll = () => {
  requestAnimationFrame(() => {
    const sections = document.querySelectorAll('#editor h1, #editor h2, #editor h3, #editor h4, #editor h5');
    const scrollY = scorllContainer.scrollTop || scorllContainer.offsetHeight;
    let currentIndex = 0;
    for (let i = 0; i < sections.length; i++) {
      // 加上浏览器 视口 的高度
      const sectionTop = (sections[i] as HTMLElement).getBoundingClientRect().top - 90;
      if (sectionTop <= 0) {
        currentIndex = i;
      }
    }

    activeIndex.value = currentIndex;

    // 滚动目录以确保当前高亮的目录项可见;
    const activeItem = document.querySelector('.table-of-contents .active') as HTMLElement;
    if (activeItem) {
      const container = document.querySelector('.table-of-contents-warp');
      const containerRect = container.getBoundingClientRect();
      const activeRect = activeItem.getBoundingClientRect();
      const scrollTop = activeItem.offsetTop - containerRect.height / 2 + activeRect.height / 2;
      container.scrollTop = scrollTop;
    }
  });
};

const getTocRight = () => {
  let width = document.body.getBoundingClientRect().width;
  if (width > 1580) {
    tocRihgt.value = `${(width - 920) / 2 + 940}px`;
  }
};

// 点击阅览
const handleView = () => {
  let urlPrefix = appStore.projectConfig.blogUrl;
  // router.push({
  //   path: '/preview',
  //   query: {
  //     id: articleId
  //   }
  // });
  window.open(urlPrefix + '/preview?id=' + articleId, '_blank');
};

// 监听路由变化，重新获取文章详情
watch(
  () => route.query.id,
  id => {
    if (id) {
      articleId = id;
      getArticleDetailFn();
    }
  }
);

// 编辑器创建完成
const handleCreated = editor => {
  editorRef.value = editor; // 记录 editor 实例，重要！
};

const resizeCallback = () => {
  getTocRight();
  state.isMobile = isMobile(1580);
};

// 模拟 ajax 异步获取内容
onMounted(() => {
  getTocRight();
  window.addEventListener('resize', resizeCallback);
  scorllContainer = document.querySelector('#layout') as HTMLElement;
  scorllContainer.addEventListener('scroll', handleScroll);
});

onUpdated(() => {
  if (updateCount.value < 2) {
    tableOfContents.value = generateTableOfContents();
  }
  addAnchorLinks();
  addCopyCodeAndMacosStyle();
  addCodeFold();
});

// 组件销毁时，也及时销毁编辑器
onBeforeUnmount(() => {
  const editor = editorRef.value;
  if (editor == null) return;
  editor.destroy();
  window.removeEventListener('resize', resizeCallback);
  scorllContainer.removeEventListener('scroll', handleScroll);
});
</script>

<style lang="scss" scoped>
.article-detail-warp {
  width: 100%;
  height: 100%;
  .article-content-warp-out {
    background-color: var(--bg-image-warp-color);
    padding: 30px 0;
    .article-content-warp {
      max-width: var(--content-max-width);
      margin: 0 auto;
      .article-content {
        padding-bottom: 20px;
        box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
        border-radius: var(--border-radius-2);
        background-color: var(--bg-content-color);
        color: var(--text-color);
        line-height: 20px !important;
        overflow: hidden;
        .article-cover-warp {
          width: 100%;
          height: 420px;
          overflow: hidden;
          .cover-warp {
            height: calc(100% - 0px);
            overflow: hidden;
          }
          .cover-title {
            height: 30px;
            line-height: 30px;
            text-align: center;
          }
        }
        .m-article-cover-warp {
          height: 240px;
        }
      }
      .article-intfo {
        margin-top: 20px;
        display: flex;
        justify-content: space-around;
        align-items: center;
        div {
          display: flex;
          justify-content: space-around;
          align-items: center;
          .iconfont {
            font-size: 20px;
            margin-right: 10px;
          }
          span {
            font-size: 16px;
            color: var(--text-color);
          }
        }
      }
    }
  }

  .no-article {
    max-width: var(--content-max-width);
    margin: 0 auto;
    text-align: center;
    height: calc(100vh - 400px);
  }
  .m-no-article {
    max-width: var(--content-max-width-m-more) !important;
    margin: 0 auto !important;
    height: calc(100vh - 300px) !important;
  }
}
</style>

<style lang="scss">
.article-content {
  pre {
    user-select: text;
    position: relative;
    .copy-button {
      user-select: none;
      position: absolute;
      top: 14px;
      right: 14px;
      border: 1px solid #333;
      padding: 2px 8px;
      color: #333;
      border-radius: 4px;
      opacity: 0;
      &:hover {
        color: var(--theme-color);
        border: 1px solid var(--theme-color);
      }
    }
    code {
      cursor: url('@/assets/images/cursor/text.png'), auto !important; // text cursor to assets/images/cursor/text.png
    }
    &:hover {
      .copy-button {
        opacity: 1;
      }
    }
    .code-title,
    .code-expand {
      user-select: none;
    }
  }
}
blockquote {
  border-radius: 2px;
}
// 目录样式
.article-content {
  position: relative;
  .flex-container {
    border-radius: 5px;
    height: calc(100% - 535px);
    width: 300px;
    background-color: var(--bg-warp-color);
    padding: 20px 10px;
    position: fixed;
    top: 430px;
    left: v-bind(tocRihgt);
    .table-of-contents-warp {
      height: 100%;
      overflow: auto;
      li {
        margin: 8px 0;
        font-size: 12px;
        a {
          text-decoration: none;
          color: var(--text-color);
          &:hover {
            color: var(--theme-color);
          }
          &.active {
            font-weight: bold;
            color: var(--theme-color);
          }
        }
      }
    }
  }
}

.m-flex-container {
  border-radius: var(--border-radius-2);
  width: 100%;
  background-color: var(--bg-warp-color);
  margin-bottom: 20px;
  padding: 20px 10px;
  overflow: hidden;
  .table-of-contents-warp {
    height: 140px;
    overflow-y: auto;
    .toc-title {
      color: var(--text-color);
    }
    li {
      margin: 8px 0;
      font-size: 12px;
      a {
        text-decoration: none;
        color: var(--text-color);
        &:hover {
          color: var(--theme-color);
        }
        &.active {
          font-weight: bold;
          color: var(--theme-color);
        }
      }
    }
  }
}
.editor-content-warp {
  img {
    border-radius: var(--border-radius-1);
  }
  .w-e-text-container {
    padding: 0 20px;
    color: var(--code-color) !important;
    background-color: var(--bg-content-color) !important;
    background-image: linear-gradient(90deg, rgba(159, 219, 252, 0.35) 3%, transparent 0), linear-gradient(1turn, rgba(159, 219, 252, 0.35) 3%, transparent 0);
    background-position: 50%;
    background-size: 20px 20px;

    pre > code {
      text-shadow: 0px 0px !important;
      background-color: var(--bg-code-color) !important;
      .token,
      .tag {
        // color: var(--code-light-color) !important;
      }
    }
  }
}

.m-article-content-warp-out {
  padding: 15px 0 !important;
  .article-content-warp {
    max-width: var(--content-max-width-m-more) !important;
  }
  .editor-content-warp {
    padding-bottom: 10px !important;
    .w-e-text-container {
      padding: 0 !important;
    }
  }
  .m-comment-view-container {
    width: var(--content-max-width-m-more) !important;
  }
}

.dark {
  .article-detail-warp {
    .code-title {
      border: 1px solid #e8e8e8;
    }
    .fold {
      border: 1px solid #e8e8e8;
    }
  }
  .w-e-text-container {
    background-image: linear-gradient(90deg, rgba(159, 219, 252, 0.08) 3%, transparent 0), linear-gradient(1turn, rgba(159, 219, 252, 0.08) 3%, transparent 0);
  }
}
</style>
